//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using System;
using System.IO;
using System.Text;
using Microsoft.Research.ScientificWorkflow.Helper;

namespace Microsoft.Research.ScientificWorkflow
{
    /// <summary>
    /// Class for reading the data downloaded from the Open DAP
    /// server to local data structures.
    /// Following is the mapping between OpenDAP Type to types
    /// supported in Trident
    /// DAP Type                        Trident Type
    /// -------------------------------------------
    /// Scalar(Int32, Float)            Corresponding C# type
    /// Array                           C# array referenced as objects
    /// Sequence                        
    /// Structure                       
    /// Grid                            Trident -> HyperCube
    /// </summary>
    class DAPDataReader
    {
        #region Constructors

        public DAPDataReader(string fileName)
        {
            dapFile = new FileStream(fileName, FileMode.Open);
        }

        #endregion

        #region Private Methods for Reading from File

        /// <summary>
        /// Reads the byteCount number of bytes from the file.
        /// The offset parameter is not used for now but is kept
        /// to support random access in future.
        /// </summary>
        private string ReadBytes(int offset, int byteCount)
        {
            try
            {
                byte[] array = new byte[byteCount];
                dapFile.Read(array, offset, byteCount);

                StringBuilder data = new StringBuilder();
                foreach (byte da in array)
                {
                    data.Append(Convert.ToChar(da));
                }
                return data.ToString();
            }
            catch (Exception ex)
            {
                System.Console.WriteLine("Unable to read bytes from file {0}", dapFile.Name);
                System.Console.WriteLine("Exception: {0}", ex.Message);
            }
            return "";
        }

        /// <summary>
        /// Method to read a single byte char from the file.
        /// </summary>
        private char ReadByteAsChar()
        {
            try
            {
                byte[] array = new byte[1];
                dapFile.Read(array, 0, 1);
                return (Convert.ToChar(array[0]));
            }
            catch (Exception ex)
            {
                System.Console.WriteLine("Unable to read bytes from file {0}", dapFile.Name);
                System.Console.WriteLine("Exception: {0}", ex.Message);
            }
            return ' ';
        }

        /// <summary>
        /// Method to read a float value from the file.
        /// </summary>
        private float ReadFloat32(int offset)
        {
            try
            {
                byte[] array = new byte[4];
                dapFile.Read(array, offset, 4);

                // Reverse the array
                byte[] arrayRev = new byte[4];
                for (int i = 0; i < array.Length; ++i)
                    arrayRev[(array.Length - 1) - i] = array[i];
                float f = BitConverter.ToSingle(arrayRev, 0);
                return f;
            }
            catch (Exception ex)
            {
                System.Console.WriteLine("Unable to read bytes from file {0}", dapFile.Name);
                System.Console.WriteLine("Exception: {0}", ex.Message);
            }
            return 0;
        }

        /// <summary>
        /// Method to read a 64 bit float value from the file.
        /// </summary>
        private double ReadFloat64(int offset)
        {
            try
            {
                byte[] array = new byte[8];
                dapFile.Read(array, offset, 8);

                // Reverse the array
                byte[] arrayRev = new byte[8];
                for (int i = 0; i < array.Length; ++i)
                    arrayRev[(array.Length - 1) - i] = array[i];
                double d = BitConverter.ToDouble(arrayRev, 0);
                return d;
            }
            catch (Exception ex)
            {
                System.Console.WriteLine("Unable to read bytes from file {0}", dapFile.Name);
                System.Console.WriteLine("Exception: {0}", ex.Message);
            }
            return 0;
        }

        /// <summary>
        /// Method to read an int value from the file.
        /// </summary>
        private int ReadInt32(int offset)
        {
            try
            {

                byte[] array = new byte[4];
                dapFile.Read(array, offset, 4);

                // Reverse the array
                byte[] arrayRev = new byte[4];
                for (int i = 0; i < array.Length; ++i)
                    arrayRev[(array.Length - 1) - i] = array[i];
                int f = BitConverter.ToInt32(arrayRev, 0);
                return f;
            }
            catch (Exception ex)
            {
                System.Console.WriteLine("Unable to read bytes from file {0}", dapFile.Name);
                System.Console.WriteLine("Exception: {0}", ex.Message);
            }
            return 0;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// This method returns the byte count for the corresponding
        /// XDR scalar types.
        /// </summary>
        private int DataSize(string Type)
        {
            switch (Type)
            {
                case "Int32":
                case "Float32":
                case "Int16":
                    return 4;
                case "Float64":
                    return 8;
                default:
                    return 0;
            }
        }

        /// <summary>
        /// This method reads the file header and advances the file pointer
        /// by 14 bytes.
        /// </summary>
        private void ReadHeaderData()
        {
            string match = "Dataset {\n    ";
            string header = ReadBytes(0, 14);

            System.Diagnostics.Debug.Assert(match == header);
        }

        /// <summary>
        /// This method parses the array definition string in the format
        /// Type VarName[name = size] and returns seperate values for 
        /// all the four strings.
        /// </summary>
        private void ReadArrayFields(string arraySpecifier, out string type,
            out string name, out string size)
        {
            type = arraySpecifier.Substring(0, arraySpecifier.IndexOf(' '));
            name = (arraySpecifier.Substring(arraySpecifier.IndexOf(' '),
                arraySpecifier.IndexOf('[') - arraySpecifier.IndexOf(' '))).Trim();
            size = (arraySpecifier.Substring(arraySpecifier.IndexOf('=') + 1,
                arraySpecifier.IndexOf(']') - arraySpecifier.IndexOf('=') - 1)).Trim();
        }

        /// <summary>
        /// This method parses the grid string to read the grid array details.
        /// </summary>
        private void ReadGridArray(ref OpenDAPURI dapURI, int index, string gridInfo)
        {
            // The string passed is of the format 
            // Grid{.      Array:. <<data>>;Maps    <<data>>
            int arrayStartIndex = gridInfo.IndexOf("Array");
            int counter = arrayStartIndex + 7;// five characters in Array plus a : and .
            while (true)
            {
                if (gridInfo[counter] != ' ')
                    break;
                counter++;
            }
            //Extract the array portion
            string arrString = gridInfo.Substring(counter, gridInfo.IndexOf(';', counter) - counter);
            string type, name, size;
            ReadArrayFields(arrString, out type, out name, out size);

            // Check if these match with those set in the query.
            if (dapURI.GetVariableInternalType(index).VariableLengths[0] < Convert.ToInt32(size)
                || dapURI.GetVariableInternalType(index).VariableNames[0] != name
                || dapURI.GetVariableInternalType(index).XDRTypes[0] != type)
            {
                throw new ArgumentException("Variables in response stream and DAP Query are not same.");
            }
        }

        /// <summary>
        /// This method parses the grid string to read the grid map details.
        /// </summary>
        private int ReadGridMaps(ref OpenDAPURI dapURI, int index, string gridInfo)
        {
            // The string passed is of the format 
            // Grid{.      Array:. <<data>>;Maps    <<data>>
            int arrayStartIndex = gridInfo.IndexOf("Maps");

            int counter = arrayStartIndex + 6;// six characters in Maps plus a ':' and '.'
            int numberOfMaps = 0;
            while (true)
            {
                while (true)
                {
                    if (gridInfo[counter] != ' ' &&
                        gridInfo[counter] != '\n')
                        break;
                    counter++;
                }
                if (gridInfo[counter] == '}')// Maps section end break
                    break;
                int endIndex = gridInfo.IndexOf(';', counter);
                string mapString = gridInfo.Substring(counter, endIndex - counter);

                string type, name, size;
                ReadArrayFields(mapString, out type, out name, out size);

                if (dapURI.GetVariableInternalType(index).VariableLengths[numberOfMaps + 1] < Convert.ToInt32(size)
                    || dapURI.GetVariableInternalType(index).VariableNames[numberOfMaps + 1] != name
                    || dapURI.GetVariableInternalType(index).XDRTypes[numberOfMaps + 1] != type)
                {
                    throw new ArgumentException("Variables in response stream and DAP Query are not same.");
                }

                counter = endIndex + 1;
                numberOfMaps++;
            }
            return numberOfMaps;
        }

        /// <summary>
        /// This method reads the data layout section of the grid.
        /// </summary>
        private void ReadGrid(ref OpenDAPURI dapURI, int index)
        {
            // Read all the characters and append in a string till
            // the last ']'
            StringBuilder gridInfo = new StringBuilder();
            while (true)
            {
                char c = this.ReadByteAsChar();
                gridInfo.Append(c);
                if (c == '}')
                    break;
            }

            ReadGridArray(ref dapURI, index, gridInfo.ToString());

            int noOfMaps = ReadGridMaps(ref dapURI, index, gridInfo.ToString());

            // Read the name of the grid
            while (true)
            {
                char c = this.ReadByteAsChar();
                if (c == ';')
                    break;
            }
        }

        /// <summary>
        /// This method verifies if the scalar type used in the URI matches
        /// with the scalar type in the response stream.
        /// </summary>
        public void CheckScalar(ref OpenDAPURI dapURI, string varInfo, int index)
        {
            int typeIndex = varInfo.IndexOf(' ');
            string varType = varInfo.Substring(0, typeIndex);
            if (varType != dapURI.GetVariableInternalType(index).XDRTypes[0])
                throw new ArgumentException("Variables Metadata Does not match");

            string varName = varInfo.Substring(typeIndex + 1);
            if (varName != dapURI.GetVariableInternalType(index).VariableNames[0])
                throw new ArgumentException("Variables Metadata Does not match");
        }

        /// <summary>
        /// This method verifies if the array type used in the URI matches
        /// with the array type in the response stream.
        /// </summary>
        public void CheckArray(ref OpenDAPURI dapURI, string varInfo, int index)
        {
            int typeIndex = varInfo.IndexOf(' ');
            string varType = varInfo.Substring(0, typeIndex);
            if (varType != dapURI.GetVariableInternalType(index).XDRTypes[0])
                throw new ArgumentException("Variables Metadata Does not match");

            int dimStartIndex = varInfo.IndexOf('[');
            string varName = varInfo.Substring(typeIndex + 1, dimStartIndex - typeIndex - 1);

            if (varName != dapURI.GetVariableInternalType(index).MainName)
                throw new ArgumentException("Variables Metadata Does not match");

            string dimDetails = varInfo.Substring(dimStartIndex);

            int startIndex = 0, lastIndex = 0;
            while (true)
            {
                lastIndex = dimDetails.IndexOf(']', startIndex);
                if (lastIndex == -1)
                    break;
                string dimData = dimDetails.Substring(startIndex, lastIndex - startIndex + 1);
                startIndex = lastIndex + 1;
            }
        }

        /// <summary>
        /// This method reads the varaible description section from the
        /// file and verifies if the variables read are the same as
        /// spepcified in the DAP Query.
        /// </summary>
        private void VerifyVariableInfo(OpenDAPURI dapURI)
        {
            // The variable information in the file is laid out in the order
            // as specified in the DDX. Based on the type of variable that is
            // to be read use the function to parse.
            for (int i = 0; i < dapURI.VariableCount; ++i)
            {
                StringBuilder variableInfo = new StringBuilder();
                if (OpenDAPURI.DapVariableType.Grid == dapURI.GetVariableType(i))
                {
                    ReadGrid(ref dapURI, i);
                    continue;
                }
                while (true)
                {
                    char c = this.ReadByteAsChar();
                    if (c == ';')
                        break;
                    variableInfo.Append(c);
                }
                if (OpenDAPURI.DapVariableType.Float64 == dapURI.GetVariableType(i) ||
                    OpenDAPURI.DapVariableType.Int16 == dapURI.GetVariableType(i) ||
                    OpenDAPURI.DapVariableType.Int32 == dapURI.GetVariableType(i) ||
                    OpenDAPURI.DapVariableType.Float32 == dapURI.GetVariableType(i))
                {
                    CheckScalar(ref dapURI, variableInfo.ToString(), i);
                }
                else if (OpenDAPURI.DapVariableType.Array == dapURI.GetVariableType(i))
                {
                    CheckArray(ref dapURI, variableInfo.ToString(), i);
                }

                // Variable definitions are seperated by 5 of delimiters. However
                // if this is the end if the variable layout section then
                // the delimiter length would be 2 bytes only
                if (i == (dapURI.VariableCount - 1))
                    this.ReadBytes(0, 2);
                else
                    this.ReadBytes(0, 5);
            }
        }

        #endregion

        #region Private methods for reading data from File

        /// <summary>
        /// Reads data values for the Grid type and returns the results
        /// in a DataHolder object.
        /// </summary>
        private DataHolder ReadGridData(int index, InternalDAPType typeDetails)
        {
            int arrayCount = typeDetails.VariableNames.Length;
            DataHolder data = new DataHolder(arrayCount, OpenDAPURI.DapVariableType.Grid);

            for (int j = 0; j < arrayCount; ++j)
            {
                // Read the array size
                int size = ReadInt32(0);
                ReadInt32(0);

                object[] valArray = new object[size];
                for (int i = 0; i < size; ++i)
                {
                    switch (typeDetails.XDRTypes[0])
                    {
                        case "Float64":
                            valArray[i] = ReadFloat64(0);
                            break;
                        case "Float32":
                            valArray[i] = ReadFloat32(0);
                            break;
                        case "Int16":
                        case "Int32":
                            valArray[i] = ReadInt32(0);
                            break;
                    }
                }
                data.AddData(j, typeDetails.VariableNames[j], valArray);
            }
            return data;
        }

        /// <summary>
        /// Reads data values for the array type and returns the results
        /// in a DataHolder object.
        /// </summary>
        private DataHolder ReadArrayData(int index, InternalDAPType typeDetails)
        {
            // Read the array size
            int size = ReadInt32(0);
            // Since the array size id written twice perform the read twice
            ReadInt32(0);

            DataHolder data = new DataHolder(1, typeDetails.DapType);
            object[] valArray = new object[size];
            for (int i = 0; i < size; ++i)
            {
                switch (typeDetails.XDRTypes[0])
                {
                    case "Float64":
                        valArray[i] = ReadFloat64(0);
                        break;
                    case "Float32":
                        valArray[i] = ReadFloat32(0);
                        break;
                    case "Int32":
                    case "Int16":
                        valArray[i] = ReadInt32(0);
                        break;
                }
            }
            data.AddData(0, typeDetails.MainName, valArray);
            return data;
        }

        /// <summary>
        /// Reads the single data value for the scalar type and
        /// returns a DataHolder object.
        /// </summary>
        private DataHolder ReadScalarData(int index, InternalDAPType typeDetails)
        {
            object value = null;
            DataHolder data = new DataHolder(typeDetails.VariableNames.Length,
                typeDetails.DapType);

            switch (typeDetails.XDRTypes[0])
            {
                case "Float64":
                    value = ReadFloat64(0);
                    break;
                case "Float32":
                    value = ReadFloat32(0);
                    break;
                case "Int16":
                case "Int32":
                    value = ReadInt32(0);
                    break;
            }
            data.AddData(index, typeDetails.MainName, new object[] { value });
            return data;
        }

        /// <summary>
        /// Reads the data values for the variables from the DAP file.
        /// </summary>
        private DataHolder[] ReadVariableValues(OpenDAPURI dapURI)
        {
            DataHolder[] dataValues = new DataHolder[dapURI.VariableCount];
            // Read the name of the file, which is terminated by
            // a ;
            while (true)
            {
                char c = this.ReadByteAsChar();
                if (c == ';')
                    break;
            }

            // Read the data header section and do nothing.
            ReadBytes(0, 7);

            for (int i = 0; i < dapURI.VariableCount; ++i)
            {
                switch (dapURI.GetVariableType(i))
                {
                    case OpenDAPURI.DapVariableType.Array:
                        dataValues[i] = this.ReadArrayData(i, dapURI.GetVariableInternalType(i));
                        break;
                    case OpenDAPURI.DapVariableType.Grid:
                        dataValues[i] = this.ReadGridData(i, dapURI.GetVariableInternalType(i));
                        break;
                    case OpenDAPURI.DapVariableType.Int16:
                    case OpenDAPURI.DapVariableType.Int32:
                    case OpenDAPURI.DapVariableType.Float32:
                    case OpenDAPURI.DapVariableType.Float64:
                        dataValues[i] = this.ReadScalarData(i, dapURI.GetVariableInternalType(i));
                        break;
                    default:
                        throw new ArgumentException("Unsupported DODS Type");
                }
            }
            return dataValues;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// This method is the entry point which parses the downloaded
        /// DAP file. This class also verifies if the contents of the file
        /// downloaded match the DAP query. If there is an inconsisteny 
        /// an exception is raised. The Parser does not provide random access
        /// to the data in the file i.e. it starts reading the file from
        /// start and finishes after all the data is read or there is an
        /// error in reading the data.
        /// </summary>
        /// <param name="dapURI"></param>
        public DataHolder[] ParseDAPFile(OpenDAPURI dapURI)
        {
            ReadHeaderData();

            VerifyVariableInfo(dapURI);

            return ReadVariableValues(dapURI);
        }

        #endregion

        #region Member Data

        /// <summary>
        /// Stream object for reading the data downloaded from the 
        /// server.
        /// </summary>
        FileStream dapFile;

        #endregion
    }

}
